package client;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import common.Message;
import common.Parser;
import common.Reply;
import common.Transition;
import common.Enums.MessageType;
import common.Enums.ProtocolState;

/**
 * @author alex and leah
 * The chat client, which will connect to the chat server and send commands to 
 * other chat clients indirectly through that server.  User must provide the site
 * to connect to as the first command-line argument.
 */
public class ChatClient
{
    private static ProtocolState currentState = null;
    private static ProtocolState expectedState = null;
    private static DataOutputStream outToServer = null;
    private static BufferedReader inFromServer = null;
    private static SSLSocket clientSocket = null;
    
    private static boolean readyToExit = false;
    
    /**
     * The entry point for the chat client.
     * @param argv Command line arguments.
     * @throws UnknownHostException
     * @throws IOException
     */
    public static void main(String argv[]) throws UnknownHostException, IOException
    {
        // The code that actually sends data across the socket is partially 
        // taken from the lecture slides and this website:
        // http://systembash.com/content/a-simple-java-tcp-server-and-tcp-client/
        // This really only makes up a few lines of code, but we are noting 
        // this here just to provide a reference.
        
        // setup the data structures
        String serverMessage = "";
        
        // create the client socket and the data streams from/to the server
        System.out.println("Establishing connection...");
        // James: Connect to host specified in command-line as 1st argument
        try 
        {
            clientSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(argv[0], 6969);
        }
        // If unknown host, print error and exit with status 1.
        catch (UnknownHostException e) 
        {
            System.out.println("Cannot connect to host " + argv[0] + ".");
            System.exit(1);
        }
        
        // James: Saving this line for testing if required
        //clientSocket = new Socket("localhost", 6969);
        outToServer = new DataOutputStream(clientSocket.getOutputStream());
        inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        
        // James: Once the connection is established, the client will be in the INITIAL state
        currentState = ProtocolState.INITIAL;
        
        // this is the thread that will handle user input
        new Thread
        (
            new Runnable()
            {
                public void run()
                {
                    BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
                    String userMessage = "";
                    
                    while(true)
                    {
                        // read an input line from the user and send it to the server
                        System.out.print("\n" + "> ");
                        try
                        {
                            userMessage = inFromUser.readLine();
                        }
                        catch (IOException e)
                        {
                        }
                        
                        synchronized (currentState)
                        {
                            if (userMessage.equals("exit"))
                            {
                                if (currentState == ProtocolState.INITIAL)
                                {
                                    readyToExit = true;
                                    
                                    // send a logout message and close the client socket
                                    Message m = new Message(MessageType.LOGOUT);
                                    System.out.println("Closing connection...");
                                    try
                                    {
                                        outToServer.writeBytes(Parser.MessageToString(m) + "\r\n");
                                        outToServer.close();
                                        inFromServer.close();
                                        clientSocket.close();
                                        System.out.println("Connection closed.");
                                    }
                                    catch (IOException e)
                                    {
                                        System.out.println("Error: Failed to close connection.");
                                    }                                    
 
                                    break;
                                }
                                else
                                {
                                    System.out.println("Error: Can only exit from INITIAL state.");
                                    continue;
                                }
                            }
                            
                            // Now need to check if the message is valid for the state we're in.
                            String type = Parser.GetNetworkMessageType(userMessage);
                            if (type == null || !type.equals("M"))
                            {
                                System.out.println("Error: Invalid command syntax.");
                                continue;
                            }
                            boolean validOutMessage = false;
                            Message outgoingMessage = Parser.StringToMessage(userMessage);
                            if (outgoingMessage == null)
                            {
                                System.out.println("Error: Invalid command syntax.");
                                continue;
                            }
                            
                            // INITIAL state checker, acceptable commands for client to send are:
                            // JOIN LOGOUT
                            if (currentState == ProtocolState.INITIAL)
                            {
                                if (outgoingMessage.GetMessageType() == MessageType.JOIN)
                                {
                                    validOutMessage = true;
                                    expectedState = null;
                                }
                                /*
                                else if (outgoingMessage.GetMessageType() == MessageType.LOGOUT)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.LOGOUT;
                                }
                                */
                            }
                            // AUTHENTICATION state checker, acceptable commands for client to send are:
                            // LOGIN LOGOUT
                            else if (currentState == ProtocolState.AUTHENTICATION)
                            {
                                if (outgoingMessage.GetMessageType() == MessageType.LOGIN)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.CONNECTED;
                                }
                                else if (outgoingMessage.GetMessageType() == MessageType.LOGOUT)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.LOGOUT;
                                }
                            }
                            // CONNECTED state checker, acceptable commands for client to send are:
                            // SEND LIST LOGOUT
                            else if (currentState == ProtocolState.CONNECTED)
                            {
                                if (outgoingMessage.GetMessageType() == MessageType.SEND)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.COMPOSITION;
                                }
                                else if (outgoingMessage.GetMessageType() == MessageType.LIST)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.CONNECTED;
                                }
                                else if (outgoingMessage.GetMessageType() == MessageType.LOGOUT)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.LOGOUT;
                                }
                            }
                            // COMPOSITION state checker, acceptable commands for client to send are:
                            // DATA CANCEL LOGOUT
                            else if (currentState == ProtocolState.COMPOSITION)
                            {
                                if (outgoingMessage.GetMessageType() == MessageType.DATA)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.COMPOSITION;
                                }
                                else if (outgoingMessage.GetMessageType() == MessageType.CANCEL)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.CONNECTED;
                                }
                                else if (outgoingMessage.GetMessageType() == MessageType.LOGOUT)
                                {
                                    validOutMessage = true;
                                    expectedState = ProtocolState.LOGOUT;
                                }
                            }
                            // There is no LOGOUT state checker as the connection is reset when this command is sent.
                            
                            // Once the command is checked against the state, send out to server or report an error.
                            if (validOutMessage)
                            {
                                try
                                {
                                    outToServer.writeBytes(Parser.MessageToString(outgoingMessage) + "\r\n");
                                }
                                catch (IOException e)
                                {
                                }
                            }
                            else if (!(validOutMessage))
                            {
                                System.out.println("Error: Invalid command for current state.");
                            }
                            else
                            {
                                System.out.println("Error: Unknown command error.");
                            }
                        }
                    }
                }
            }
        ).start();
        
        // continue looping until the client reaches the LOGOUT state
        // this thread handles reading data sent from the server
        boolean checkState = false;
        synchronized (currentState)
        {
            checkState = !readyToExit;
        }
        while(checkState)
        {
            // get the server's response and output it to the screen
            // James: since we're working in a raw data stream set up here in this
            // developing protocol it makes sense to see everything sent from the server.
            // When the protocol is mature the client should probably defend the user from seeing
            // bad messages from the server or display a user-friendly error message rather than
            // scrambled text.
            try
            {
                serverMessage = inFromServer.readLine();
            }
            catch (SocketException e)
            {
                // this will happen if the socket is closed
                break;
            }
     
            synchronized (currentState)
            {
                if (readyToExit)
                {
                    break;
                }
                
                DateFormat dateFormat = new SimpleDateFormat("(HH:mm:ss)");
                String time = dateFormat.format(new Date());
                System.out.print("\n" + time + ": " + serverMessage);
                System.out.print("\n" + "> ");
                
                if (serverMessage == null)
                {
                    continue;
                }
                
                // First check to see if this is a server reply code. If so, we need
                // to check to see if a state transition has occurred.
                String networkMessageType = Parser.GetNetworkMessageType(serverMessage);
                if (networkMessageType == null)
                {
                    // do nothing
                }
                else if (networkMessageType.equals("R"))
                {
                    Reply incomingReply = Parser.StringToReply(serverMessage);
                    if (incomingReply != null)
                    {
                        // 102: Positive reply to LOGIN
                        if (incomingReply.GetId() == 102 && 
                            expectedState == ProtocolState.CONNECTED)
                        {
                            if (Transition.TransitionCheck(currentState, expectedState))
                                currentState = expectedState;
                        }
                        // 103: Positive reply to LOGOUT
                        else if (incomingReply.GetId() == 103 &&
                                 expectedState == ProtocolState.LOGOUT)
                        {
                            if (Transition.TransitionCheck(currentState, expectedState))
                                currentState = expectedState;
                        }
                        // 105: Positive reply to SEND
                        else if (incomingReply.GetId() == 105 &&
                                 expectedState == ProtocolState.COMPOSITION)
                        {
                            if (Transition.TransitionCheck(currentState, expectedState))
                                currentState = expectedState;
                        }
                        // 106: Positive reply to DATA
                        else if (incomingReply.GetId() == 106 &&
                                 expectedState == ProtocolState.CONNECTED)
                        {
                            if (Transition.TransitionCheck(currentState, expectedState))
                                currentState = expectedState;
                        }
                        // 107: Positive reply to CANCEL
                        else if (incomingReply.GetId() == 107 &&
                                 expectedState == ProtocolState.CONNECTED)
                        {
                            if (Transition.TransitionCheck(currentState, expectedState))
                                currentState = expectedState;
                        }
                        // 112: Positive reply to LOGOUT
                        else if (incomingReply.GetId() == 112 &&
                                currentState == ProtocolState.LOGOUT)
                        {
                            if (Transition.TransitionCheck(ProtocolState.LOGOUT, ProtocolState.INITIAL))
                            {
                                currentState = ProtocolState.INITIAL;
                                expectedState = null;
                                
                                try
                                {
                                    clientSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(argv[0], 6969);
                                }
                                // If unknown host, print error and exit with status 1.
                                catch (UnknownHostException e) 
                                {
                                    System.out.println("Cannot connect to host " + argv[0] + ".");
                                    System.exit(1);
                                }
                                // James: Saving this line for testing if required
                                //clientSocket = new Socket("localhost", 6969);
                                outToServer.close();
                                inFromServer.close();
                                outToServer = new DataOutputStream(clientSocket.getOutputStream());
                                inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                            }
                        }
                    }
                }
                // If not a reply code, see if it is a server command. State
                // checking also required with the server command in some scenarios.
                else if (networkMessageType.equals("M"))
                {
                    Message incomingMessage = Parser.StringToMessage(serverMessage);
                    if (incomingMessage != null)
                    {
                        // When AUTHENTICATE received during INITIAL state, send 101
                        // message, or 401 if in the wrong state.
                        if (incomingMessage.GetMessageType() == MessageType.AUTHENTICATE)
                        {
                            if (currentState == ProtocolState.INITIAL)
                            {
                                currentState = ProtocolState.AUTHENTICATION;
                                Reply r = new Reply(101, "AUTHENTICATE received");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                            else
                            {
                                Reply r = new Reply(401, "AUTHENTICATE not appropriate here");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                        }
                        // When INCOMING received during CONNECTED/COMPOSITION
                        // state, send 108 message or 408 if in the wrong state.
                        if (incomingMessage.GetMessageType() == MessageType.INCOMING)
                        {
                            if (currentState == ProtocolState.CONNECTED || 
                                currentState == ProtocolState.COMPOSITION)
                            {
                                Reply r = new Reply(108, "INCOMING message received");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                            else
                            {
                                Reply r = new Reply(408, "INCOMING message not appropriate here");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                        }
                        // When ALIVE received during
                        // AUTHENTICATION/CONNECTED/COMPOSITION state, send 110
                        // message or 410 if in the wrong state.
                        if (incomingMessage.GetMessageType() == MessageType.ALIVE)
                        {
                            if (currentState == ProtocolState.AUTHENTICATION || 
                                currentState == ProtocolState.CONNECTED || 
                                currentState == ProtocolState.COMPOSITION)
                            {
                                Reply r = new Reply(110, "Still alive");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                            else
                            {
                                Reply r = new Reply(410, "Idle check not appropriate here");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                        }
                        // When FAILED received during CONNECTED/COMPOSITION state,
                        // send 111 message or 411 if in the wrong state.
                        if (incomingMessage.GetMessageType() == MessageType.FAILED)
                        {
                            if (currentState == ProtocolState.CONNECTED || 
                                currentState == ProtocolState.COMPOSITION)
                            {
                                Reply r = new Reply(111, "Copy FAILED message");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                            else
                            {
                                Reply r = new Reply(411, "FAILED message not appropriate here");
                                outToServer.writeBytes(Parser.ReplyToString(r) + "\r\n");
                            }
                        }
                    }
                }
                else
                {
                    System.out.println("Error: Invalid message type defined!");
                }
                // The message could be a non-transitioning message or an invalid
                // reply/command from the server.
                // In any of these cases the client is not required to respond.
                
                checkState = !readyToExit;
            }
        } // end of main while loop here
    }
}